/*
    https://wangdoc.com/javascript/types/function.html
    函数
 */

// 1.概述
// 1.1 函数的声明
// JavaScript 有三种声明函数的方法。

// (1) function命令
// function命令声明的代码区块，就是一个函数。function命令后面是函数名，函数名后面是一对圆括号，里面是传入函数的参数。函数体放在大括号里面
function print(s) {
    console.log(s);
}
// 上面的代码命名了一个print函数，以后使用print()这种形式，就可以调用相应的代码。这叫做函数的声明（Function Declaration）

// (2) 函数表达式
// 除了用function命令声明函数，还可以采用变量赋值的写法
var print = function (s) {
    console.log(s);
};
// 这种写法将一个匿名函数赋值给变量。这时，这个匿名函数又称函数表达式（Function Expression），因为赋值语句的等号右侧只能放表达式

// 采用函数表达式声明函数时，function命令后面不带有函数名。如果加上函数名，该函数名只在函数体内部有效，在函数体外部无效
var print = function x(){
    console.log(typeof x);
};

x
// ReferenceError: x is not defined

print()
// function

// 上面代码在函数表达式中，加入了函数名x。这个x只在函数体内部可用，指代函数表达式本身，其他地方都不可用。
// 这种写法的用处有两个，一是可以在函数体内部调用自身，二是方便除错（除错工具显示函数调用栈时，将显示函数名，而不再显示这里是一个匿名函数）
// 因此，下面的形式声明函数也非常常见。
var f = function f() {};

// 需要注意的是，函数的表达式需要在语句的结尾加上分号，表示语句结束。
// 而函数的声明在结尾的大括号后面不用加分号。总的来说，这两种声明函数的方式，差别很细微，可以近似认为是等价的。

// （3）Function 构造函数
// 第三种声明函数的方式是Function构造函数
var add = new Function(
    'x',
    'y',
    'return x + y'
);

// 等同于
function add(x, y) {
    return x + y;
}

// 上面代码中，Function构造函数接受三个参数，除了最后一个参数是add函数的“函数体”，其他参数都是add函数的参数。
// 你可以传递任意数量的参数给Function构造函数，只有最后一个参数会被当做函数体，如果只有一个参数，该参数就是函数体。

var foo = new Function(
    'return "hello world";'
);

// 等同于
function foo() {
    return 'hello world';
}

// Function构造函数可以不使用new命令，返回结果完全一样。
// 总的来说，这种声明函数的方式非常不直观，几乎无人使用。

// 1.2 函数的重复声明
// 如果同一个函数被多次声明，后面的声明就会覆盖前面的声明
function f() {
    console.log(1);
}
f() // 2

function f() {
    console.log(2);
}
f() // 2
//上面代码中，后一次的函数声明覆盖了前面一次。而且，由于函数名的提升（参见下文），前一次声明在任何时候都是无效的，这一点要特别注意。

// 1.3 圆括号运算符，return 语句和递归
// 调用函数时，要使用圆括号运算符。圆括号之中，可以加入函数的参数
function add(x, y) {
    return x + y;
}

add(1, 1) // 2

// 上面代码中，函数名后面紧跟一对圆括号，就会调用这个函数。
// 函数体内部的return语句，表示返回。JavaScript 引擎遇到return语句，就直接返回return后面的那个表达式的值，后面即使还有语句，也不会得到执行。
// 也就是说，return语句所带的那个表达式，就是函数的返回值。return语句不是必需的，如果没有的话，该函数就不返回任何值，或者说返回undefined。
// 函数可以调用自身，这就是递归（recursion）。下面就是通过递归，计算斐波那契数列的代码。
function fib(num) {
    if (num === 0) return 0;
    if (num === 1) return 1;
    return fib(num - 2) + fib(num - 1);
}

fib(6);

// 上面代码中，fib函数内部又调用了fib，计算得到斐波那契数列的第6个元素是8

// 1.4 第一等公民
// JavaScript 语言将函数看作一种值，与其它值（数值、字符串、布尔值等等）地位相同。凡是可以使用值的地方，就能使用函数。
// 比如，可以把函数赋值给变量和对象的属性，也可以当作参数传入其他函数，或者作为函数的结果返回。函数只是一个可以执行的值，此外并无特殊之处。
// 由于函数与其他数据类型地位平等，所以在 JavaScript 语言中又称函数为第一等公民。
function add(x, y) {
    return x + y;
}

// 将函数赋值给一个变量
var operator = add;

// 将函数作为参数和返回值
function a(op) {
    return op;
}

a(add)(1, 1);
// 2

// 1.5 函数名的提升
// JavaScript 引擎将函数名视同变量名，所以采用function命令声明函数时，整个函数会像变量声明一样，被提升到代码头部。所以，下面的代码不会报错
f();
function f() {}
// 表面上，上面代码好像在声明之前就调用了函数f。但是实际上，由于“变量提升”，函数f被提升到了代码头部，也就是在调用之前已经声明了。
// 但是，如果采用赋值语句定义函数，JavaScript 就会报错。
f();
var f = function (){};
// TypeError: undefined is not a function
// 上面的代码等同于下面的形式。
var f;
f();
f = function () {};
// 上面代码第二行，调用f的时候，f只是被声明了，还没有被赋值，等于undefined，所以会报错。

// 注意，如果像下面例子那样，采用function命令和var赋值语句声明同一个函数，由于存在函数提升，最后会采用var赋值语句的定义
var f = function () {
    console.log('1');
}

function f() {
    console.log('2');
}

f() // 1
// 上面例子中，表面上后面声明的函数f，应该覆盖前面的var赋值语句，但是由于存在函数提升，实际上正好反过来

// 2.函数的属性和方法

// 2.1 name属性
// 函数的name属性返回函数的名字
function f1() {}
f1.name // "f1"

// 如果是通过变量赋值定义的函数，那么name属性返回变量名
var f2 = function () {};
f2.name // "f2"

// 但是，上面这种情况，只有在变量的值是一个匿名函数时才是如此。如果变量的值是一个具名函数，那么name属性返回function关键字之后的那个函数名。
var f3 = function myName() {};
f3.name // 'myName'

// name属性的一个用处，就是获取参数函数的名字
var myFunc = function () {};

function test(f) {
    console.log(f.name);
}

test(myFunc) // myFunc

// 2.2 length属性
// 函数的length属性返回函数预期传入的参数个数, 即函数定义之中的参数个数
function f(a, b) {}
f.length // 2

// 上面代码定义了空函数f，它的length属性就是定义时的参数个数。不管调用时输入了多少个参数，length属性始终等于2。
// length属性提供了一种机制，判断定义时和调用时参数的差异，以便实现面向对象编程的“方法重载”（overload）。

// 2.3 toString()
// 函数的toString()方法返回一个字符串，内容是函数的源码
function f() {
    a();
    b();
    c();
}

f.toString()
// function f() {
//  a();
//  b();
//  c();
// }

// 对于那些原生的函数，toString()方法返回function (){[native code]}。
Math.sqrt.toString()
// "function sqrt() { [native code] }"

// 上面代码中，Math.sqrt()是 JavaScript 引擎提供的原生函数，toString()方法就返回原生代码的提示。

// 函数内部的注释也可以返回。
function f() {/*
  这是一个
  多行注释
*/}

f.toString()
// "function f(){/*
//   这是一个
//   多行注释
// */}"

var multiline = function (fn) {
    var arr = fn.toString().split('\n');
    return arr.slice(1, arr.length - 1).join('\n');
};

function f() {/*
  这是一个
  多行注释
*/}

multiline(f);
// " 这是一个
//   多行注释"

// 3.函数作用域

// 3.1 定义
// 作用域（scope）指的是变量存在的范围。在 ES5 的规范中，JavaScript 只有两种作用域：一种是全局作用域，变量在整个程序中一直存在，所有地方都可以读取；另一种是函数作用域，变量只在函数内部存在。ES6 又新增了块级作用域，本教程不涉及。
// 对于顶层函数来说，函数外部声明的变量就是全局变量（global variable），它可以在函数内部读取。
var v = 1;

function f() {
    console.log(v);
}

f()
// 1

// 上面的代码表明，函数f内部可以读取全局变量v。
// 在函数内部定义的变量，外部无法读取，称为“局部变量”（local variable）
function f(){
    var v = 1;
}
v // ReferenceError: v is not defined

// 上面代码中，变量v在函数内部定义，所以是一个局部变量，函数之外就无法读取。
// 函数内部定义的变量，会在该作用域内覆盖同名全局变量。

var v = 1;

function f(){
    var v = 2;
    console.log(v);
}

f() // 2
v // 1

// 上面代码中，变量v同时在函数的外部和内部有定义。结果，在函数内部定义，局部变量v覆盖了全局变量v。

// 注意，对于var命令来说，局部变量只能在函数内部声明，在其他区块中声明，一律都是全局变量。
if (true) {
    var x = 5;
}
console.log(x);  // 5
// 上面代码中，变量x在条件判断区块之中声明，结果就是一个全局变量，可以在区块之外读取。

// 3.2 函数内部的变量提升
// 与全局作用域一样，函数作用域内部也会产生“变量提升”现象。var命令声明的变量，不管在什么位置，变量声明都会被提升到函数体的头部
function foo(x) {
    if (x > 100) {
        var tmp = x - 100;
    }
}

// 等同于
function foo(x) {
    var tmp;
    if (x > 100) {
        tmp = x - 100;
    };
}

// 3.3 函数本身的作用域
// 函数本身也是一个值，也有自己的作用域。它的作用域与变量一样，就是其声明时所在的作用域，与其运行时所在的作用域无关。
var a = 1;
var x = function () {
    console.log(a);
};

function f() {
    var a = 2;
    x();
}

f() // 1

// 上面代码中，函数x是在函数f的外部声明的，所以它的作用域绑定外层，内部变量a不会到函数f体内取值，所以输出1，而不是2。
// 总之，函数执行时所在的作用域，是定义时的作用域，而不是调用时所在的作用域。
// 很容易犯错的一点是，如果函数A调用函数B，却没考虑到函数B不会引用函数A的内部变量。

var x = function () {
    console.log(a);
};

function y(f) {
    var a = 2;
    f();
}

y(x)
// ReferenceError: a is not defined

// 上面代码将函数x作为参数，传入函数y。但是，函数x是在函数y体外声明的，作用域绑定外层，因此找不到函数y的内部变量a，导致报错。
// 同样的，函数体内部声明的函数，作用域绑定函数体内部。
function foo() {
    var x = 1;
    function bar() {
        console.log(x);
    }
    return bar;
}

var x = 2;
var f = foo();
f() // 1
// 上面代码中，函数foo内部声明了一个函数bar，bar的作用域绑定foo。当我们在foo外部取出bar执行时，变量x指向的是foo内部的x，而不是foo外部的x。
// 正是这种机制，构成了下文要讲解的“闭包”现象。

// 4. 参数
// 4.1 概述
// 函数运行的时候，有时需要提供外部数据，不同的外部数据会得到不同的结果，这种外部数据就叫参数
function square(x) {
    return x * x;
}

square(2); // 4
square(3); // 9

// 上式的x就是square函数的参数。每次运行的时候，需要提供这个值，否则得不到结果。

// 4.2 参数的省略
// 函数参数不是必需的，JavaScript 允许省略参数。
function f(a, b) {
    return a;
}

f(1, 2, 3) // 1
f(1) // 1
f() // undefined

f.length // 2

// 上面代码的函数f定义了两个参数，但是运行时无论提供多少个参数（或者不提供参数），JavaScript 都不会报错。
// 省略的参数的值就变为undefined。需要注意的是，函数的length属性与实际传入的参数个数无关，只反映函数预期传入的参数个数

// 但是，没有办法只省略靠前的参数，而保留靠后的参数。如果一定要省略靠前的参数，只有显式传入undefined。
function f(a, b) {
    return a;
}

// f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined
// 上面代码中，如果省略第一个参数，就会报错。

// 4.3 传递方式
// 函数参数如果是原始类型的值（数值、字符串、布尔值），传递方式是传值传递（passes by value）。这意味着，在函数体内修改参数值，不会影响到函数外部。
var p = 2;

function f(p) {
    p = 3;
}
f(p);

p // 2
// 上面代码中，变量p是一个原始类型的值，传入函数f的方式是传值传递。因此，在函数内部，p的值是原始值的拷贝，无论怎么修改，都不会影响到原始值。
// 但是，如果函数参数是复合类型的值（数组、对象、其他函数），传递方式是传址传递（pass by reference）。
// 也就是说，传入函数的原始值的地址，因此在函数内部修改参数，将会影响到原始值。
var obj = { p: 1 };

function f(o) {
    o.p = 2;
}
f(obj);

obj.p // 2
// 上面代码中，传入函数f的是参数对象obj的地址。因此，在函数内部修改obj的属性p，会影响到原始值。

// 注意，如果函数内部修改的，不是参数对象的某个属性，而是替换掉整个参数，这时不会影响到原始值
var obj = [1, 2, 3];

function f(o) {
    o = [2, 3, 4];
}
f(obj);

obj // [1, 2, 3]
// 上面代码中，在函数f()内部，参数对象obj被整个替换成另一个值。这时不会影响到原始值。
// 这是因为，形式参数（o）的值实际是参数obj的地址，重新对o赋值导致o指向另一个地址，保存在原地址上的值当然不受影响。
// 有意思,没想到在java中也是同样的表现,看来自己之前理解错了

// 4.4 同名参数
// 如果有同名的参数，则取最后出现的那个值
function f(a, a) {
    console.log(a);
}

f(1, 2) // 2

// 调用函数f()的时候，没有提供第二个参数，a的取值就变成了undefined。这时，如果要获得第一个a的值，可以使用arguments对象
function f(a, a) {
    console.log(arguments[0]);
}

f(1) // 1

// 4.5 arguments对象

// (1) 定义
// 由于 JavaScript 允许函数有不定数目的参数，所以需要一种机制，可以在函数体内部读取所有参数。这就是arguments对象的由来。
// arguments对象包含了函数运行时的所有参数，arguments[0]就是第一个参数，arguments[1]就是第二个参数，以此类推。这个对象只有在函数体内部，才可以使用。
var f = function (one) {
    console.log(arguments[0]);
    console.log(arguments[1]);
    console.log(arguments[2]);
}

f(1, 2, 3)
// 1
// 2
// 3

// 正常模式下，arguments对象可以在运行时修改。
var f = function(a, b) {
    arguments[0] = 3;
    arguments[1] = 2;
    return a + b;
}

f(1, 1) // 5

// 上面代码中，函数f()调用时传入的参数，在函数内部被修改成3和2。
// 严格模式下，arguments对象与函数参数不具有联动关系。也就是说，修改arguments对象不会影响到实际的函数参数。
var f = function(a, b) {
    'use strict'; // 开启严格模式
    arguments[0] = 3;
    arguments[1] = 2;
    return a + b;
}

f(1, 1) // 2
// 上面代码中，函数体内是严格模式，这时修改arguments对象，不会影响到真实参数a和b

// 通过arguments对象的length属性，可以判断函数调用时到底带几个参数
function f() {
    return arguments.length;
}

f(1, 2, 3) // 3
f(1) // 1
f() // 0

// (2) 与数组的关系
// 需要注意的是，虽然arguments很像数组，但它是一个对象。数组专有的方法（比如slice和forEach），不能在arguments对象上直接使用。
// 如果要让arguments对象使用数组方法，真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法：slice方法和逐一填入新数组。
var arguments = {};
var args = Array.prototype.slice.call(arguments);

// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
    args.push(arguments[i]);
}

// (3) callee 属性
// arguments对象带有一个callee属性，返回它所对应的原函数。
var f = function () {
    console.log(arguments.callee === f);
}

f() // true
// 可以通过arguments.callee，达到调用函数自身的目的。这个属性在严格模式里面是禁用的，因此不建议使用

// 5.函数的其他知识点
// 5.1 闭包
// 闭包（closure）是 JavaScript 语言的一个难点，也是它的特色，很多高级应用都要依靠闭包实现。